<?php

namespace Modules\Wechat\Model\Dao\Db;

use Dao\Db;
use Modules\Wechat\Model\Util\Dao\Db as Util;
use Modules\Wechat\Model\Util\Exception\Dao\Db as Exception;


/**
 * 数据库抽象类
 *
 * @package Modules\Wechat\Model\Dao\Db
 */
abstract class Abstraction extends Db
{
    /** @const string 默认表名前缀 */
    const TABLE_PREFIX = 'wc_';

    /** @const string 默认主键字段名 */
    const PK_FIELD = 'id';

    /** @var string 数据库名称 */
    protected $schema_name = self::DB_NAME;

    /** @var string 表名前缀 */
    protected $table_prefix = self::TABLE_PREFIX;

    /** @var string 当前DAO对应的数据表完整名称 */
    protected $table_name = '';

    /** @var string 当前DAO对应数据表的主键 */
    protected $pk_field = self::PK_FIELD;

    /** @var array 表中所有字段 */
    protected $columns = [];

    /** @var array 表中需要加密的字段 */
    protected $column_encrypted = [];

    final public function __construct()
    {
        return $this->init();
    }

    /**
     * 初始化方法
     *
     * 此方法会在实例化时执行,在此方法中设置数据表名前缀\数据表名称\加密字段\主键字段等.
     *
     * @return mixed
     */
    abstract protected function init();

    /**
     * 设置数据库名称
     *
     * @param string $name 数据库名称
     *
     * @return $this
     */
    public function setSchemaName($name)
    {
        $this->schema_name = $name;
        return $this;
    }

    /**
     * 获取当前数据名名称
     *
     * @return string
     */
    public function getSchemaName()
    {
        return $this->schema_name;
    }

    /**
     * 设置数据表名称前缀
     *
     * @param string $prefix 前缀:不会自动补全下划线,设置是什么就是什么
     *
     * @return $this
     */
    public function setTablePrefix($prefix)
    {
        $this->table_prefix = strval($prefix);
        return $this;
    }

    /**
     * 获取当前数据表名称前缀
     *
     * @return string
     */
    public function getTablePrefix()
    {
        return $this->table_prefix;
    }

    /**
     * 设置当前DAO的数据表名称
     *
     * @param string $name           数据表名称:一般不含前缀
     * @param bool   $prepend_prefix 是否添加前缀
     *
     * @return $this
     */
    public function setTableName($name, $prepend_prefix = true)
    {
        if ($prepend_prefix) {
            $this->table_name = "{$this->getTablePrefix()}{$name}";
        } else {
            $this->table_name = $name;
        }
        return $this;
    }

    /**
     * 获取当前DAO是数据表名称
     *
     * @return string
     */
    public function getTableName()
    {
        return $this->table_name;
    }

    /**
     * 设置需要加密的列
     *
     * @param string|array $column_names 一个或多个列的名称
     * @param bool         $overwrite    是否覆盖已设置的列名
     *
     * @return $this
     */
    public function setEncryptedColumns($column_names, $overwrite = false)
    {
        if (is_array($column_names)) {
            static::$encrypt_keys = $overwrite
                ? $column_names
                : array_merge(static::$encrypt_keys, $column_names);
        } else {
            static::$encrypt_keys = $overwrite
                ? [$column_names]
                : array_merge(static::$encrypt_keys, [$column_names]);
        }
        return $this;
    }

    /**
     * 获取所有已设置的需要加密的列名
     *
     * @return array
     */
    public function getEncryptedColumns()
    {
        return static::$encrypt_keys;
    }

    /**
     * 检查某列是否需要加密
     *
     * @param string $column_name 列名
     *
     * @return bool
     */
    protected function isEncryptedColumn($column_name)
    {
        return false !== array_search($column_name, static::$encrypt_keys);
    }

    /**
     * 检查当前DAO是否有某个属性/列
     *
     * @param string $name 属性名
     *
     * @return bool
     */
    protected function hasProperty($name)
    {
        return Util::hasColumn($this, $name);
    }

    /**
     * 设置属性的值
     *
     * @param string $name  属性名
     * @param string $value 属性值
     *
     * @return $this
     * @throws \Modules\Wechat\Model\Util\Exception\Dao\Db
     */
    public function setProperty($name, $value)
    {
        if ($this->hasProperty($name)) {
            $this->$name = $value;
        } else {
            (new Exception(Exception::CODE_COLUMN_NOT_EXISTS, ['@column@' => $name]))->throwMe();
        }
        return $this;
    }

    /* 获取属性的值
     *
     * @param string $name 属性名
     *
     * @return mixed
     * @throws \S\WeChat\Abstraction\Exception
     */
    public function getProperty($name)
    {
        if (!$this->hasProperty($name)) {
            (new Exception(Exception::CODE_COLUMN_NOT_EXISTS, ['@column@' => $name]))->throwMe();
        }
        return $this->$name;
    }

    /**
     * 设置多个属性的值
     *
     * @param array $properties   属性名和值的键值对
     * @param bool  $without_null 是否过滤掉值为null的属性
     *
     * @return $this
     */
    public function setProperties(array $properties, $without_null = true)
    {
        foreach ($properties as $name => $value) {
            if ($without_null && null === $value) {
                continue;
            }
            $this->setProperty($name, $value);
        }
        return $this;
    }

    /**
     * 获取多个属性的值
     *
     * @param array $property_names 需要获取其值的属性名:为空时获取所有属性的值
     * @param bool  $without_null   是否过滤掉值为null的属性
     *
     * @return array
     */
    public function getProperties(array $property_names = [], $without_null = true)
    {
        if (empty($property_names)) {
            return Util::getColumns($this, $without_null);
        }
        $return = [];
        foreach ($property_names as $name) {
            $value = $this->getProperty($name);
            if ($without_null && null === $value) {
                continue;
            }
            $return[$name] = $value;
        }
        return $return;
    }

    /**
     * 重设所有属性的值
     *
     * @return $this
     */
    public function resetProperties()
    {
        foreach ($this->getAllColumn() as $name) {
            $this->setProperty($name, null);
        }
        return $this;
    }

    /**
     * 获取所有列的名称
     *
     * @return array
     */
    protected function getAllColumn()
    {
        return array_keys($this->getProperties([], false));
    }

    /**
     * 设置主键字段名
     *
     * @param string $column_name 字段名
     *
     * @return $this
     */
    public function setPkField($column_name)
    {
        $this->pk_field = $column_name;
        return $this;
    }

    /**
     * 获取主键字段名
     *
     * @return string
     */
    public function getPkField()
    {
        return $this->pk_field;
    }

    /**
     * 设置主键的值
     *
     * @param mixed $value 主键的值
     *
     * @return Abstraction
     */
    public function setPkValue($value)
    {
        return $this->setProperty($this->getPkField(), $value);
    }

    /**
     * 获取主键的值
     *
     * @return mixed
     */
    public function getPkValue()
    {
        return $this->getProperty($this->getPkField());
    }

    /**
     * 根据当前DAO对象的属性的值创建数据库记录
     *
     * @param bool $return_pk    是否返回新数据的主键
     * @param bool $replace_into 是否覆盖插入
     *
     * @return bool|int
     * @throws \S\Exception
     */
    public function create($return_pk = true, $replace_into = false)
    {
        $result = self::db($this->getSchemaName())->insert(
            $this->getTableName(),
            self::encrypt($this->getProperties()),
            $return_pk,
            $replace_into
        );
        if ($return_pk) {
            return $this->setPkValue($result)->getPkValue();
        }
        return $result;
    }

    /**
     * 将当前DAO的值保存到数据库
     *
     * @param array $conditions 保存时的条件
     * @param int   $limit      保存条数限制
     * @param bool  $affect_row 是否返回受影响的行数
     *
     * @return bool|int
     * @throws \S\Exception
     */
    public function update(array $conditions = [], $limit = 0, $affect_row = false)
    {
        return self::db($this->getSchemaName())->update(
            $this->getTableName(),
            self::encrypt($this->getProperties()),
            self::encrypt($conditions),
            $limit,
            $affect_row
        );
    }

    /**
     * 根据主键将当前DAO的值保存到数据库
     *
     * @param string $pk 主键:如果为空则从当前DAO对象获取主键
     *
     * @return bool
     * @throws \Modules\Wechat\Model\Util\Exception\Dao\Db
     */
    public function updateByPk($pk = '')
    {
        $pk_field = $this->getPkField();
        $pk_value = $pk ?: $this->getProperty($pk_field);
        if (empty($pk_value)) {
            (new Exception(Exception::CODE_PK_REQUIRED_UPDATING))->throwMe();
        }
        return $this->setPkValue(null)->update([$pk_field => $pk_value]);
    }

    /**
     * 根据当前DAO对象属性查询所有数据
     *
     * @param array $columns 需要返回的列:为空时返回所有列
     * @param array $order   排序方式
     * @param int   $limit   返回条数
     * @param int   $offset  跳过条数
     * @param int   $fetch   返回数据的格式
     *
     * @return array|string
     */
    public function read($columns = [], array $order = [], $limit = 0, $offset = 0, $fetch = \PDO::FETCH_ASSOC)
    {
        if (!is_array($columns)) {
            $columns = [$columns];
        }
        $data = self::db($this->getSchemaName())->query(
            $this->getTableName(),
            self::encrypt($this->getProperties()),
            empty($columns) ? $this->getAllColumn() : $columns,
            $order,
            $this->getLimitOffset($limit, $offset),
            $fetch
        );
        return empty($data) ? [] : self::decrypt($data);
    }

    protected function getLimitOffset($limit, $offset)
    {
        $limit  = 0 < $limit ? 0 : $limit;
        $offset = 0 < $offset ? 0 : $offset;
        if ($limit) {
            return $offset ? [$limit, $offset] : $limit;
        }
        return $limit;
    }

    /**
     * 根据当前DAO对象属性查询一条数据
     *
     * @param array $columns 需要返回的列:为空时返回所有列
     * @param array $order   排序方式
     *
     * @return array
     */
    public function readRow($columns = [], array $order = [])
    {
        if ($data = $this->read($columns, $order, 1, \PDO::FETCH_ASSOC)) {
            return $data[0];
        }
        return [];
    }

    /**
     * 根据当前DAO对象属性查询数据中的一个字段
     *
     * @param string $column_name 需要查询的字段名
     * @param array  $order       排序方式
     *
     * @return string
     */
    public function readScalar($column_name, array $order = [])
    {
        if ($this->hasProperty($column_name)) {
            if ($data = $this->readRow([$column_name], $order)) {
                return $data[$column_name];
            }
        }
        return '';
    }

    /**
     * 根据当前DAO对象属性进行统计
     *
     * @param string $column   需要统计的列:如果为空则统计主键
     * @param bool   $distinct 是否去重
     *
     * @return bool|mixed
     */
    public function count($column = '', $distinct = false)
    {
        return self::db($this->getSchemaName())->count(
            $this->getTableName(),
            self::encrypt($this->getProperties()),
            empty($column) ? $this->getPkField() : $column,
            $distinct
        );
    }

    /**
     * 根据当前DAO对象查询一列数据
     *
     * @param string $column_name 列名:如果为空则返回主键
     * @param array  $order       排序方式
     * @param int    $limit       返回条数
     * @param int    $offset      跳过条数
     *
     * @return array
     */
    public function readColumn($column_name = '', array $order = [], $limit = 0, $offset = 0)
    {
        $return = [];
        if (empty($column_name)) {
            $column_name = $this->getPkField();
        }
        if ($data = $this->read([$column_name], $order, $limit, $offset)) {
            foreach ($data as $d) {
                $return[] = $d[$column_name];
            }
            return $return;
        }
        return $return;
    }

    /**
     * 根据主键查询数据
     *
     * @param array|string $pk      主键:如果为空则从当前DAO获取
     * @param array        $columns 需要返回的列:为空时返回所有列
     *
     * @return array|string
     */
    public function readByPk($pk = '', array $columns = [])
    {
        if (empty($pk)) {
            $pk = $this->getPkValue();
        }
        return $this->resetProperties()->setPkValue($pk)->readRow($columns);
    }

    /**
     * 根据主键查询一条数据
     *
     * @param string $pk 主键:如果为空则从当前DAO获取
     *
     * @return array
     */
    public function readOneByPk($pk = '')
    {
        if (empty($pk)) {
            $pk = $this->getPkValue();
        }
        if (is_array($pk) && 1 < count($pk)) {
            (new Exception(Exception::CODE_TOO_MUCH_PK))->throwMe();
        }
        if ($data = $this->readByPk($pk)) {
            return $data[0];
        }
        return [];
    }

    /**
     * 根据当前DAO对象属性的值删除数据
     *
     * @param bool $affect_row 是否返回被删除的行数
     * @param int  $limit      删除数据条数限制:为0时不限制
     *
     * @return bool|int
     * @throws \S\Exception
     */
    public function delete($affect_row = false, $limit = 0)
    {
        return self::db($this->getSchemaName())->delete(
            $this->getTableName(),
            self::encrypt($this->getProperties()),
            $limit,
            $affect_row
        );
    }

    /**
     * 通过主键删除数据
     *
     * @param string $pk 主键:如果为空则从当前DAO对象获取主键
     *
     * @return int
     */
    public function deleteByPk($pk = '')
    {
        $pk = $pk ?: $this->getPkValue();
        return $this->resetProperties()->setPkValue($pk)->delete();
    }

}